/*
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.dspl.validate;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.dspl.model.Attribute;
import com.google.dspl.model.Concept;
import com.google.dspl.model.Concept.ConceptProperty;
import com.google.dspl.model.DataType;
import com.google.dspl.model.Dataset;
import com.google.dspl.model.Identifier;
import com.google.dspl.model.Table;
import com.google.dspl.model.Table.Column;
import com.google.dspl.model.Topic;
import com.google.dspl.validate.ConceptInheritenceTraverser.ConceptNode;

import java.util.List;
import java.util.Map;

/**
 * This class contains a set of datasets and helps in resolving references to any item from one of
 * them. This class also contains methods that infer information about a concept that might not be
 * explicitly specified in the concept definition but might instead need to be inferred by following
 * its inheritance chain and possibly also following references to other datasets.
 *
 * @author Shardul Deo
 */
public final class DatasetResolver {

  private final Map<String, Dataset> datasets = Maps.newConcurrentMap();

  /**
   * Add a dataset to this resolver. This allows the resolver to be able to follow references to
   * items in the dataset.
   */
  public void addDataset(Dataset dataset) {
    datasets.put(dataset.getDatasetId(), dataset);
  }

  public void removeDataset(String datasetId) {
    datasets.remove(datasetId);
  }

  public Dataset getDataset(String datasetId) {
    return datasets.get(datasetId);
  }

  /**
   * @return Concept definition if present in this resolver, null otherwise.
   */
  public Concept getConcept(Identifier conceptId) {
    Dataset dataset = datasets.get(conceptId.getDatasetId());
    if (dataset != null) {
      for (Concept concept : dataset.getConcepts()) {
        if (Objects.equal(conceptId.getObjectId(), concept.getId())) {
          return concept;
        }
      }
    }
    return null;
  }

  /**
   * @return the property information defined in the youngest concept that defines the property.
   *     It does not include any property information from any parent concept.
   */
  public ConceptProperty getConceptProperty(Identifier conceptId, String propertyId) {
    checkNotNull(conceptId);
    checkNotNull(propertyId);
    Concept concept = getConcept(conceptId);
    if (concept != null) {
      ConceptNodeImpl conceptNode = new ConceptNodeImpl(conceptId.getDatasetId(), concept);
      return ConceptInheritenceTraverser.getConceptProperty(conceptNode, propertyId);
    }
    return null;
  }

  /**
   * @return Topic definition if present in this resolver, null otherwise.
   */
  public Topic getTopic(Identifier topicId) {
    Dataset dataset = datasets.get(topicId.getDatasetId());
    if (dataset != null) {
      return findTopic(topicId, dataset.getTopics());
    }
    return null;
  }

  private Topic findTopic(Identifier topicId, List<Topic> topics) {
    for (Topic topic : topics) {
      if (topicId.getObjectId().equals(topic.getId())) {
        return topic;
      }
      Topic childTopic = findTopic(topicId, topic.getChildTopics());
      if (childTopic != null) {
        return childTopic;
      }
    }
    return null;
  }

  /**
   * @return Table definition if present in this resolver, null otherwise.
   */
  public Table getTable(Identifier tableId) {
    Dataset dataset = datasets.get(tableId.getDatasetId());
    if (dataset != null) {
      for (Table table : dataset.getTables()) {
        if (tableId.getObjectId().equals(table.getId())) {
          return table;
        }
      }
    }
    return null;
  }

  public Column getColumn(Identifier tableId, String columnId) {
    Table table = getTable(tableId);
    if (table != null) {
      for (Column column : table.getColumns()) {
        if (columnId.equals(column.getId())) {
          return column;
        }
      }
    }
    return null;
  }

  /**
   * Fetch all of the properties of the given concept, including the properties of its parent
   * concepts.  These properties are returned in a multimap keyed on the id of the concept.
   *
   * @param conceptId Concept whose properties are desired.
   * @return Multimap of concept properties, keyed by the concept that defines that property.
   *
   * TODO: Determine a better API for this notion, especially around the inheritance of properties.
   */
  public Multimap<Identifier, ConceptProperty> getPropertiesIncludingInherited(
          Identifier conceptId) {
    checkNotNull(conceptId);
    Concept concept = getConcept(conceptId);
    if (concept != null) {
      ConceptNodeImpl conceptNode = new ConceptNodeImpl(conceptId.getDatasetId(), concept);
      return ConceptInheritenceTraverser.getPropertiesIncludingInherited(conceptNode);
    }
    return null;
  }

  public boolean isParent(String datasetId, Concept concept, Identifier possibleParent) {
    ConceptNodeImpl conceptNode = new ConceptNodeImpl(datasetId, concept);
    return ConceptInheritenceTraverser.isParent(conceptNode, possibleParent);
  }

  /**
   * @return Data type of the concept, either explicitly specified or inferred through its
   *         inheritance chain, or null if the type cannot be inferred.
   */
  public DataType resolveConceptType(String datasetId, Concept concept) {
    return ConceptInheritenceTraverser.resolveConceptType(new ConceptNodeImpl(datasetId, concept));
  }


  /**
   * @return Data table of the concept, either explicitly specified or inferred through its
   *         inheritance chain, or null if a table cannot be inferred.
   */
  public Identifier resolveConceptTable(String datasetId, Concept concept) {
    return ConceptInheritenceTraverser.resolveConceptTable(new ConceptNodeImpl(datasetId, concept));
  }


  /**
   * @return Data type of an attribute in the given concept, either explicitly specified or
   *         inferred through its inheritance chain, or null if the type cannot be inferred.
   */
  public DataType resolveConceptAttributeType(
      String datasetId, String attributeId, Concept concept) {
    ConceptNodeImpl conceptNode = new ConceptNodeImpl(datasetId, concept);
    return ConceptInheritenceTraverser.resolveConceptAttributeType(attributeId, conceptNode);
  }

  /**
   * @return Concept referenced by an attribute in the given concept, either explicitly specified or
   *         inferred through its inheritance chain, or null if it cannot be inferred.
   */
  public Identifier resolveConceptAttributeReferencedConcept(
      String datasetId, String attributeId, Concept concept) {
    ConceptNodeImpl conceptNode = new ConceptNodeImpl(datasetId, concept);
    return ConceptInheritenceTraverser.resolveConceptAttributeReferencedConcept(
        attributeId, conceptNode);
  }

  /**
   * @return Data type of a property in the given concept, either explicitly specified or inferred
   *         through its inheritance chain, or null if the type cannot be inferred.
   */
  public DataType resolveConceptPropertyType(String datasetId, String propertyId, Concept concept) {
    return ConceptInheritenceTraverser.resolveConceptPropertyType(
        propertyId, new ConceptNodeImpl(datasetId, concept));
  }

  /**
   * @return A {@link ConceptNode} view of a concept present in this resolver, useful for calling
   *         {@link ConceptInheritenceTraverser} directly.
   */
  public ConceptNode getConceptNode(Identifier conceptId) {
    Concept concept = getConcept(conceptId);
    if (concept != null) {
      return new ConceptNodeImpl(conceptId.getDatasetId(), concept);
    }
    return null;
  }

  /**
   * View of a concept for use in the {@link ConceptInheritenceTraverser}.
   */
  private class ConceptNodeImpl implements ConceptNode {

    private final String datasetId;
    private final Concept concept;

    private ConceptNodeImpl(String datasetId, Concept concept) {
      this.datasetId = Preconditions.checkNotNull(datasetId);
      this.concept = Preconditions.checkNotNull(concept);
    }

    @Override
    public Identifier getId() {
      return new Identifier(datasetId, concept.getId());
    }

    @Override
    public boolean isParent(Identifier parentId) {
      return parentId.equals(concept.getParent());
    }

    @Override
    public ConceptNode getParent() {
      if (concept.getParent() != null) {
        return getConceptNode(concept.getParent());
      }
      return null;
    }

    @Override
    public DataType getType() {
      return concept.getType();
    }

    private DataType resolveConceptType(Identifier conceptId) {
      Concept concept = getConcept(conceptId);
      if (concept != null) {
        return DatasetResolver.this.resolveConceptType(datasetId, concept);
      }
      return null;
    }

    @Override
    public DataType getAttributeType(String attributeId) {
      Attribute attribute = getAttribute(attributeId);
      if (attribute != null) {
        // Get its type, either explicit or from concept
        if (attribute.getType() != null) {
          return attribute.getType();
        }
        if (attribute.getConcept() != null) {
          return resolveConceptType(attribute.getConcept());
        }
      }
      return null;
    }

    @Override
    public Identifier getConceptReferencedByAttribute(String attributeId) {
      Attribute attribute = getAttribute(attributeId);
      if (attribute != null) {
        // Get referenced concept
        return attribute.getConcept();
      }
      return null;
    }

    private Attribute getAttribute(String attributeId) {
      // Find matching attribute
      for (Attribute attribute : concept.getAttributes()) {
        String id = null;
        if (attribute.getId() != null) {
          id = attribute.getId();
        } else if (attribute.getConcept() != null) {
          id = attribute.getConcept().getObjectId();
        }
        if (attributeId.equals(id)) {
          return attribute;
        }
      }
      return null;
    }

    @Override
    public DataType getPropertyType(String propertyId) {
      // Find matching property
      for (ConceptProperty property : concept.getProperties()) {
        String id = null;
        if (property.getId() != null) {
          id = property.getId();
        } else if (property.getConcept() != null) {
          id = property.getConcept().getObjectId();
        }
        if (propertyId.equals(id)) {
          // Get its type, either explicit or from concept
          if (property.getType() != null) {
            return property.getType();
          }
          if (property.getConcept() != null) {
            return resolveConceptType(property.getConcept());
          }
        }
      }
      return null;
    }

    @Override
    public List<ConceptProperty> getProperties() {
      return concept.getProperties();
    }

    @Override
    public String getTable() {
      return (concept.getTable() != null) ? concept.getTable().getTableId() : null;
    }
  }
}
